package fr.lelouet.tools.regression.simple;

import java.util.*;
import java.util.Map.Entry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import flanagan.analysis.Regression;
import fr.lelouet.tools.regression.Data;
import fr.lelouet.tools.regression.Result;
import fr.lelouet.tools.regression.Solver;

public class SimpleSolver implements Solver {

	private static final Logger logger = LoggerFactory
			.getLogger(SimpleSolver.class);

	protected Map<String, DimensionInformations> dimensions = new HashMap<String, DimensionInformations>();

	private DimensionInformations outputInfos = new DimensionInformations();

	private List<Data> data = new ArrayList<Data>();

	/** add a created data, and uses its internal values */
	private void addCopiedData(Data dat) {
		for (Entry<String, Double> e : dat.getInput().entrySet()) {
			dimensions.get(e.getKey()).containsValue(e.getValue());
		}
		outputInfos.containsValue(dat.getOutput());
		data.add(dat);
	}

	/**
	 * copy a map of input dimensions, and an output value, into the internal
	 * memory with no check
	 */
	protected Data directAddData(Map<String, Double> input, double output) {
		Data ret = new Data();
		Map<String, Double> vals = new HashMap<String, Double>();
		vals.putAll(input);
		ret.setInput(vals);
		ret.setOutput(output);
		addCopiedData(ret);
		return ret;
	}

	/**
	 * copy data that may contain dimensions not to be registered. The excessive
	 * dimensions are discarded.
	 * 
	 * @return the real added data
	 */
	protected Data addExcessiveData(Map<String, Double> input, double output) {
		Data ret = new Data();
		Map<String, Double> vals = new HashMap<String, Double>();
		vals.putAll(input);
		vals.keySet().retainAll(dimensions.keySet());
		ret.setInput(vals);
		ret.setOutput(output);
		addCopiedData(ret);
		return ret;
	}

	@Override
	public void removeDimensions(Set<String> toRemove) {
		dimensions.keySet().removeAll(toRemove);
	}

	@Override
	public Data addData(Map<String, Double> input, double output) {
		if (dimensions.keySet().containsAll(input.keySet())
				&& input.keySet().containsAll(dimensions.keySet())) {
			return directAddData(input, output);
		} else {
			logger.warn(
					"not all dimensions in the solver : expected {}, got {}",
					new Object[] { dimensions.keySet(), input.keySet() });
			return null;
		}
	}

	@Override
	public boolean addOrReduce(Map<String, Double> input, double output) {
		Set<String> lostDimensions = new HashSet<String>();
		lostDimensions.addAll(dimensions.keySet());
		lostDimensions.removeAll(input.keySet());
		if (!lostDimensions.isEmpty()) {
			logger.debug("discarding dimensions <{}> when adding <{}> ",
					lostDimensions, input);
		}
		removeDimensions(lostDimensions);
		Data added = addExcessiveData(input, output);
		return lostDimensions.size() == 0
				&& added.getInput().size() == dimensions.size();

	}

	@Override
	public List<Data> getData() {
		return data;
	}

	@Override
	public int getDataNumber() {
		return getData().size();
	}

	@Override
	public Set<String> getDimensions() {
		return Collections.unmodifiableSet(dimensions.keySet());
	}

	@Override
	public void setDimensions(String... dimensions) {
		Set<String> toRemove = new HashSet<String>();
		toRemove.addAll(getDimensions());
		List<String> list = Arrays.asList(dimensions);
		toRemove.removeAll(list);
		removeDimensions(toRemove);
		if (dimensions.length != getDimensions().size()) {
			data.clear();
			this.dimensions.clear();
			for (String s : list) {
				this.dimensions.put(s, new DimensionInformations());
			}
		}

	}

	@Override
	public Result solve() {
		Map<String, Integer> positions = new HashMap<String, Integer>();
		for (String s : getDimensions()) {
			positions.put(s, positions.size());
		}

		double[][] xdata = convertInputTabular(positions);
		double[] ydata = convertOutputTabular();
		Regression reg = solveTabular(xdata, ydata);
		Result r = new Result();
		// logger.debug("result error size is : "
		// + reg.getBestEstimatesErrors().length + " while using "
		// + getDimensions().size() + " dimensions and " + getDataNumber()
		// + " data");
		for (String s : getDimensions()) {
			int pos = positions.get(s) + 1;
			r.getEstimates().put(s, reg.getBestEstimates()[pos]);
			r.getErrors().put(s, reg.getBestEstimatesErrors()[pos]);
		}
		r.setConstantEstimate(reg.getBestEstimates()[0]);
		return r;
	}

	/**
	 * put the data outputs in an array of doubles.
	 * 
	 * @return a double dimension array of the inputs with format
	 *         outputs[dimensionNumber][snapshotNumber], snapshotNumber being
	 *         the numeral order of added data.(firs is 0, third is 2, etc…)
	 * @param positions
	 *            the map of each dimension to the index it must be stored in
	 *            the returned array (dimensionName -> dimensionNumber)
	 */
	protected double[][] convertInputTabular(Map<String, Integer> positions) {
		double[][] xdata = new double[positions.size()][getDataNumber()];
		for (int dataNum = 0; dataNum < data.size(); dataNum++) {
			Data d = data.get(dataNum);
			for (String s : getDimensions()) {
				int dimensionNum = positions.get(s);
				xdata[dimensionNum][dataNum] = d.getInput().get(s);
			}
		}
		return xdata;
	}

	/**
	 * put the data inputs in a array of doubles
	 * 
	 * @return the array of input values from the data, in the order they were
	 *         added
	 */
	protected double[] convertOutputTabular() {
		double[] ydata = new double[getDataNumber()];
		for (int dataNum = 0; dataNum < data.size(); dataNum++) {
			Data d = data.get(dataNum);
			ydata[dataNum] = d.getOutput();
		}
		return ydata;
	}

	protected Regression solveTabular(double[][] xdata, double[] ydata) {
		Regression reg = new Regression(xdata, ydata);
		// reg.ignoreDofFcheck();
		try {
			reg.linear();
		} catch (Exception e) {
			StringBuilder sb = new StringBuilder("<table>\n");
			for (int i = 0; i < xdata[0].length; i++) {
				sb.append(" <tr>");
				for (double[] element : xdata) {
					sb.append("<td>").append(element[i]).append("</td>");
				}
				sb.append("<td /><td>").append(ydata[i]).append("</td></tr>\n");

			}
			sb.append("</table>");
			logger.debug("while solving : \n" + sb, e);
			reg.ignoreDofFcheck();
			reg.linear();
		}
		return reg;
	}

	@Override
	public Set<String> getUncompleteDimensions() {
		Set<String> ret = new HashSet<String>();
		for (Entry<String, DimensionInformations> e : dimensions.entrySet()) {
			if (!e.getValue().hasEnoughRange()) {
				ret.add(e.getKey());
			}
		}
		return ret;
	}

	@Override
	public boolean enoughOutputValues() {
		logger.trace("dimension info for ouput : {}", outputInfos);
		return outputInfos.hasEnoughRange();
	}

}
